diff options
Diffstat (limited to 'src/routes/[username]/settings')
| -rw-r--r-- | src/routes/[username]/settings/+page.server.js | 4 | ||||
| -rw-r--r-- | src/routes/[username]/settings/+page.svelte | 41 | ||||
| -rw-r--r-- | src/routes/[username]/settings/_account.svelte | 35 | ||||
| -rw-r--r-- | src/routes/[username]/settings/_security.svelte | 109 |
4 files changed, 155 insertions, 34 deletions
diff --git a/src/routes/[username]/settings/+page.server.js b/src/routes/[username]/settings/+page.server.js index 7053a939..ae7e0a78 100644 --- a/src/routes/[username]/settings/+page.server.js +++ b/src/routes/[username]/settings/+page.server.js @@ -3,9 +3,11 @@ import { auth, get, post } from '$lib/utils'; export const actions = { default: async ({ cookies, request }) => { + let form = Object.fromEntries(await request.formData()); + let user = { ...(await get('/me', auth(cookies))), - ...Object.fromEntries(await request.formData()) + ...form }; try { diff --git a/src/routes/[username]/settings/+page.svelte b/src/routes/[username]/settings/+page.svelte index c1ac20d0..7bd4ddcc 100644 --- a/src/routes/[username]/settings/+page.svelte +++ b/src/routes/[username]/settings/+page.svelte @@ -1,21 +1,28 @@ <script> + import { fly } from 'svelte/transition'; import { enhance } from '$app/forms'; import Account from './_account.svelte'; import Pos from './_pos.svelte'; import Security from './_security.svelte'; - import { Icon } from '$comp'; + import { Icon, Spinner, Pin } from '$comp'; import { t } from '$lib/translations'; import { failure, success } from '$lib/utils'; - import { avatarUpload, bannerUpload } from '$lib/store'; + import { avatarUpload, bannerUpload, pin } from '$lib/store'; import { upload } from '$lib/upload'; export let data; + export let form; let { user, rates, token } = data; $: update(data); let update = () => ({ user, rates, token } = data); + $: if (form?.message?.startsWith('Pin')) { + failure('Wrong pin, try again'); + $pin = ''; + } + let tab = 'account'; let tabs = [ @@ -26,23 +33,37 @@ $: ({ comp } = tabs.find((t) => t.name === tab)); - let save = async () => { + let loading, submit; + let save = async (msg) => { + loading = true; + if (typeof msg !== 'string') msg = 'Settings saved'; + try { if ($avatarUpload) { await upload($avatarUpload.file, $avatarUpload.type, $avatarUpload.progress, token); } + if ($bannerUpload) { await upload($bannerUpload.file, $bannerUpload.type, $bannerUpload.progress, token); } - success('Settings saved'); + + success(msg); } catch (e) { console.log(e); failure('Something went wrong'); } + + loading = false; }; </script> +{#if user.haspin && $pin?.length !== 6} + <Pin /> +{/if} + <form method="POST" class="mb-[154px]" use:enhance on:submit={save}> + <input type="hidden" name="pin" value={$pin} /> + <div class="mt-24 mb-20 px-3 md:px-0 w-full md:w-[400px] mx-auto space-y-8"> <h1 class="text-center text-3xl md:text-4xl font-semibold mb-10"> {$t('user.settings.header')} @@ -58,16 +79,22 @@ {/each} </div> - <svelte:component this={comp} {user} {rates} /> + <svelte:component this={comp} {user} {rates} {submit} /> </div> <div class="z-10 fixed bottom-0 bg-white shadow border-t w-full flex justify-center items-center py-3" > <button + bind:this={submit} type="submit" - class="bg-black text-white rounded-xl font-semibold mx-auto py-3 w-40 hover:opacity-80" + class="border-2 border-black rounded-xl font-semibold mx-auto py-3 w-40 hover:opacity-80" + class:bg-black={loading} > - <div class="my-auto">Save Settings</div> + {#if loading} + <Spinner /> + {:else} + <div class="my-auto">Save Settings</div> + {/if} </button> </div> </form> diff --git a/src/routes/[username]/settings/_account.svelte b/src/routes/[username]/settings/_account.svelte index bb229b93..f139b3f4 100644 --- a/src/routes/[username]/settings/_account.svelte +++ b/src/routes/[username]/settings/_account.svelte @@ -17,6 +17,7 @@ $: banner = files.banner.src; $: $tempProfileFiles = { profile: files.profile.src, banner: files.banner.src }; + let togglePassword = () => (revealPassword = !revealPassword); let colorThemes = [ { theme: 1, color1: 'from-[#F5F7FA]', color2: 'to-[#C3CFE2]' }, { theme: 2, color1: 'from-[#FDFCFB]', color2: 'to-[#E2D1C3]' }, @@ -36,9 +37,14 @@ percent = Math.round((event.loaded / event.total) * 100); }; + let tooLarge = {}; + let files = { profile: {}, banner: {} }; let handleFile = async ({ target }, type) => { + tooLarge[type] = false; let file = target.files[0]; + console.log('TYPE', file.size); + if (file.size > 5000000) return (tooLarge[type] = true); var reader = new FileReader(); reader.onload = async (e) => { files[type].src = e.target.result; @@ -83,7 +89,8 @@ {/if} <button type="button" - on:click={() => (revealPassword = !revealPassword)} + on:click={togglePassword} + on:keydown={togglePassword} class="absolute right-5 top-11" > <Icon icon={revealPassword ? 'eye' : 'eye-off'} /> @@ -118,11 +125,13 @@ <div> <span class="font-bold">{$t('user.settings.profileImage')}</span> + <div class="flex"> {#if profile} <div class="relative rounded-full overflow-hidden text-center w-20 h-20 my-auto hover:opacity-80 cursor-pointer" on:click={() => select('profile')} + on:keydown={() => select('profile')} > <img src={profile} @@ -134,9 +143,10 @@ <div class="relative rounded-full overflow-hidden text-center w-20 h-20 my-auto hover:opacity-80 cursor-pointer" on:click={() => select('profile')} + on:keydown={() => select('profile')} > <img - src={`/api/public/${user.username}-profile.png`} + src={`/api/public/${user.username}-profile.webp`} class="absolute w-full h-full object-cover object-center visible overflow-hidden" alt={user.username} /> @@ -145,6 +155,7 @@ <div class="rounded-full border-4 border-white p-4 bg-gradient-to-r {$colorTheme} w-24 my-auto hover:opacity-80 cursor-pointer" on:click={() => select('profile')} + on:keydown={() => select('profile')} > <Icon icon="logo-symbol-white" style="mx-auto" /> </div> @@ -154,7 +165,8 @@ <button type="button" class="border rounded-2xl font-bold w-24 text-center px-0 py-2 hover:opacity-80" - on:click={() => select('profile')}>Select</button + on:click={() => select('profile')} + on:keydown={() => select('profile')}>Select</button > <input type="file" @@ -167,6 +179,10 @@ {/if} </div> </div> + + {#if tooLarge['profile']} + <div class="text-red-600">Max file size 5MB</div> + {/if} </div> <div> @@ -180,19 +196,22 @@ src={banner} class="w-full object-cover object-center visible overflow-hidden h-48 mb-4 hover:opacity-80" on:click={() => select('banner')} + on:keydown={() => select('banner')} alt="Banner" /> {:else if user.banner} <img - src={`/api/public/${user.username}-banner.png`} + src={`/api/public/${user.username}-banner.webp`} class="w-full object-cover object-center visible overflow-hidden h-48 mb-4 hover:opacity-80" on:click={() => select('banner')} + on:keydown={() => select('banner')} alt="Banner" /> {:else} <div class="bg-gradient-to-r {$colorTheme} w-full h-48 mb-4 cursor-pointer hover:opacity-80" on:click={() => select('banner')} + on:keydown={() => select('banner')} alt="Banner" /> {/if} @@ -201,7 +220,8 @@ <button type="button" class="border rounded-2xl font-bold w-24 text-center px-0 py-2 hover:opacity-80" - on:click={() => select('banner')}>Select</button + on:click={() => select('banner')} + on:keydown={() => select('banner')}>Select</button > <input type="file" @@ -212,6 +232,10 @@ {#if files.banner.filename} <div class="mt-2 text-sm">{files.banner.filename}</div> {/if} + + {#if tooLarge['banner']} + <div class="text-red-600">Max file size 5MB</div> + {/if} </div> <!-- TODO @@ -226,6 +250,7 @@ ? 'border-black' : ''}" on:click={() => handleThemeClick(colors)} + on:keydown={() => handleThemeClick(colors)} /> {/each} </div> --> diff --git a/src/routes/[username]/settings/_security.svelte b/src/routes/[username]/settings/_security.svelte index 2a490aa4..98b9b503 100644 --- a/src/routes/[username]/settings/_security.svelte +++ b/src/routes/[username]/settings/_security.svelte @@ -4,26 +4,80 @@ import { Icon, Qr } from '$comp'; import { Pincode, PincodeInput } from 'svelte-pincode'; import { post, success, failure } from '$lib/utils'; + import { pin as current } from '$lib/store'; - export let user; + export let user, submit; let settingPin, setting2fa, disabling2fa; - let code = []; - let pin = ''; let token = ''; - let tokenInput, pinInput; + let pinCode = []; + let verifyCode = []; + let tokenInput, pinInput, verifyInput; + let verifying = false; + let old; + let startVerifying = () => { + verifying = true; + }; + + $: pin = pinCode.join(''); + + let locked; + $: verifyCode.join('').length > 5 && checkPin(); + let checkPin = async () => { + old = $current; + if (locked) return; + locked = true; + + let verify = verifyCode.join(''); + + pinCode = ['', '', '', '', '', '']; + verifyCode = ['', '', '', '', '', '']; + settingPin = false; + + try { + if (pin.length > 5 && pin === verify) { + user.haspin = true; + $current = pin; + submit.click(); + } else { + failure('Pin mismatch, try again'); + settingPin = true; + pinInput.focusFirstInput(); + } + } catch (e) { + failure('Problem setting PIN'); + } + + verifying = false; + + await new Promise((r) => setTimeout(r, 500)); + locked = false; + }; + + $: if (verifying) verifyInput && tick().then(verifyInput.focusFirstInput); $: if (settingPin) pinInput && tick().then(pinInput.focusFirstInput); - $: if (setting2fa || disabling2fa) tokenInput && tick().then(tokenInput.focusFirstInput); + $: if (setting2fa || disabling2fa) tokenInput && tick().then(tokenInput?.focusFirstInput); let reset = () => { token = ''; - code = []; return true; }; - let togglePin = () => (settingPin = !settingPin); + let togglePin = async () => { + if (user.haspin) { + try { + pin = null; + submit.click(); + } catch (e) { + failure('Failed to disable pin'); + } + } else { + settingPin = true; + } + }; + let toggleEnabling = () => reset() && (setting2fa = !setting2fa); let toggleDisabling = () => reset() && (disabling2fa = !disabling2fa); @@ -58,31 +112,44 @@ $: otpUri = `otpauth://totp/coinos:${user.username}?secret=${user.otpsecret}&period=30&digits=6&algorithm=SHA1&issuer=coinos`; </script> -<!-- TODO <div> - <span class="font-bold mb-1">{$t('user.settings.securityPIN')}</span> + <input type="hidden" name="newpin" value={pin} /> + + <span class="font-bold mb-1">{verifying ? 'Verify' : 'Security'} PIN</span> <p class="text-secondary mb-1"> {$t('user.settings.securityPINDescription')} </p> {#if settingPin} <div class="flex my-4"> <div class="mx-auto"> - <Pincode bind:code bind:value={user.pin} bind:this={pinInput}> - <PincodeInput /> - <PincodeInput /> - <PincodeInput /> - <PincodeInput /> - <PincodeInput /> - <PincodeInput /> - </Pincode> + <div class:hidden={verifying}> + <Pincode on:complete={startVerifying} bind:code={pinCode} bind:this={pinInput}> + <PincodeInput /> + <PincodeInput /> + <PincodeInput /> + <PincodeInput /> + <PincodeInput /> + <PincodeInput /> + </Pincode> + </div> + <div class:hidden={!verifying}> + <Pincode bind:code={verifyCode} bind:this={verifyInput}> + <PincodeInput /> + <PincodeInput /> + <PincodeInput /> + <PincodeInput /> + <PincodeInput /> + <PincodeInput /> + </Pincode> + </div> </div> </div> {:else} <button type="button" class="primary" on:click={togglePin} - ><Icon icon="lock" style="mr-1" /> {$t('user.settings.setPIN')}</button + ><Icon icon="lock" style="mr-1" /> {user.haspin ? 'Disable Pin' : 'Enable Pin'}</button > {/if} -</div> --> +</div> <!-- TODO <div> @@ -116,7 +183,7 @@ <div class="text-center my-4"> {$t('user.settings.oneTimeCode')}<br /> - <Pincode bind:code bind:value={token} bind:this={tokenInput}> + <Pincode bind:value={token} bind:this={tokenInput}> <PincodeInput /> <PincodeInput /> <PincodeInput /> @@ -128,7 +195,7 @@ {:else if disabling2fa} <div class="text-center my-4"> {$t('user.settings.oneTimeCode')}<br /> - <Pincode bind:code bind:value={token} bind:this={tokenInput}> + <Pincode bind:value={token} bind:this={tokenInput}> <PincodeInput /> <PincodeInput /> <PincodeInput /> |
